{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# LLM 에 도구 바인딩(Binding Tools)\n",
    "\n",
    "LLM 모델이 도구(tool) 를 호출할 수 있으려면 chat 요청을 할 때 모델에 도구 스키마(tool schema) 를 전달해야 합니다. \n",
    "\n",
    "도구 호출(tool calling) 기능을 지원하는 LangChain Chat Model 은 `.bind_tools()` 메서드를 구현하여 LangChain 도구 객체, Pydantic 클래스 또는 JSON 스키마 목록을 수신하고 공급자별 예상 형식으로 채팅 모델에 바인딩(binding) 합니다.\n",
    "\n",
    "바인딩된 Chat Model 의 후속 호출은 모델 API에 대한 모든 호출에 도구 스키마를 포함합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# API KEY를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API KEY 정보로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH15-Bind-Tools\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## LLM에 바인딩할 Tool 정의\n",
    "\n",
    "실험을 위한 도구(tool) 를 정의합니다.\n",
    "\n",
    "- `get_word_length` : 단어의 길이를 반환하는 함수\n",
    "- `add_function` : 두 숫자를 더하는 함수\n",
    "- `naver_news_crawl` : 네이버 뉴스 기사를 크롤링하여 본문 내용을 반환하는 함수\n",
    "\n",
    "**참고**\n",
    "- 도구를 정의할 때 `@tool` 데코레이터를 사용하여 도구를 정의합니다.\n",
    "- docstring 은 가급적 영어로 작성하는 것을 권장합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "import re\n",
    "import requests\n",
    "from bs4 import BeautifulSoup\n",
    "from langchain.agents import tool\n",
    "\n",
    "\n",
    "# 도구를 정의합니다.\n",
    "@tool\n",
    "def get_word_length(word: str) -> int:\n",
    "    \"\"\"Returns the length of a word.\"\"\"\n",
    "    return len(word)\n",
    "\n",
    "\n",
    "@tool\n",
    "def add_function(a: float, b: float) -> float:\n",
    "    \"\"\"Adds two numbers together.\"\"\"\n",
    "    return a + b\n",
    "\n",
    "\n",
    "@tool\n",
    "def naver_news_crawl(news_url: str) -> str:\n",
    "    \"\"\"Crawls a 네이버 (naver.com) news article and returns the body content.\"\"\"\n",
    "    # HTTP GET 요청 보내기\n",
    "    response = requests.get(news_url)\n",
    "\n",
    "    # 요청이 성공했는지 확인\n",
    "    if response.status_code == 200:\n",
    "        # BeautifulSoup을 사용하여 HTML 파싱\n",
    "        soup = BeautifulSoup(response.text, \"html.parser\")\n",
    "\n",
    "        # 원하는 정보 추출\n",
    "        title = soup.find(\"h2\", id=\"title_area\").get_text()\n",
    "        content = soup.find(\"div\", id=\"contents\").get_text()\n",
    "        cleaned_title = re.sub(r\"\\n{2,}\", \"\\n\", title)\n",
    "        cleaned_content = re.sub(r\"\\n{2,}\", \"\\n\", content)\n",
    "    else:\n",
    "        print(f\"HTTP 요청 실패. 응답 코드: {response.status_code}\")\n",
    "\n",
    "    return f\"{cleaned_title}\\n{cleaned_content}\"\n",
    "\n",
    "\n",
    "tools = [get_word_length, add_function, naver_news_crawl]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## bind_tools() 로 LLM 에 도구 바인딩\n",
    "\n",
    "llm 모델에 `bind_tools()` 를 사용하여 도구를 바인딩합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# 모델 생성\n",
    "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n",
    "\n",
    "# 도구 바인딩\n",
    "llm_with_tools = llm.bind_tools(tools)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "실행결과를 확인합니다. \n",
    "\n",
    "결과는 `tool_calls` 에 저장됩니다. 따라서, `.tool_calls` 를 확인하여 도구 호출 결과를 확인할 수 있습니다.\n",
    "\n",
    "**참고**\n",
    "- `name` 은 도구의 이름을 의미합니다.\n",
    "- `args` 는 도구에 전달되는 인자를 의미합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 실행 결과\n",
    "llm_with_tools.invoke(\"What is the length of the word 'teddynote'?\").tool_calls"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "다음으로는 `llm_with_tools` 와 `JsonOutputToolsParser` 를 연결하여 `tool_calls` 를 parsing 하여 결과를 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.output_parsers.openai_tools import JsonOutputToolsParser\n",
    "\n",
    "# 도구 바인딩 + 도구 파서\n",
    "chain = llm_with_tools | JsonOutputToolsParser(tools=tools)\n",
    "\n",
    "# 실행 결과\n",
    "tool_call_results = chain.invoke(\"What is the length of the word 'teddynote'?\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(tool_call_results)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "실행 결과는 다음과 같습니다.\n",
    "\n",
    "**참고**\n",
    "- `type`: 도구의 이름\n",
    "- `args`: 도구에 전달되는 인자"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(tool_call_results, end=\"\\n\\n==========\\n\\n\")\n",
    "# 첫 번째 도구 호출 결과\n",
    "single_result = tool_call_results[0]\n",
    "# 도구 이름\n",
    "print(single_result[\"type\"])\n",
    "# 도구 인자\n",
    "print(single_result[\"args\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "도구 이름과 일치하는 도구를 찾아 실행합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tool_call_results[0][\"type\"], tools[0].name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`execute_tool_calls` 함수는 도구를 찾아 args 를 전달하여 도구를 실행합니다.\n",
    "\n",
    "즉, `type` 은 도구의 이름을 의미하고 `args` 는 도구에 전달되는 인자를 의미합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def execute_tool_calls(tool_call_results):\n",
    "    \"\"\"\n",
    "    도구 호출 결과를 실행하는 함수\n",
    "\n",
    "    :param tool_call_results: 도구 호출 결과 리스트\n",
    "    :param tools: 사용 가능한 도구 리스트\n",
    "    \"\"\"\n",
    "    # 도구 호출 결과 리스트를 순회합니다.\n",
    "    for tool_call_result in tool_call_results:\n",
    "        # 도구의 이름과 인자를 추출합니다.\n",
    "        tool_name = tool_call_result[\"type\"]  # 도구의 이름(함수명)\n",
    "        tool_args = tool_call_result[\"args\"]  # 도구에 전달되는 인자\n",
    "\n",
    "        # 도구 이름과 일치하는 도구를 찾아 실행합니다.\n",
    "        # next() 함수를 사용하여 일치하는 첫 번째 도구를 찾습니다.\n",
    "        matching_tool = next((tool for tool in tools if tool.name == tool_name), None)\n",
    "\n",
    "        if matching_tool:\n",
    "            # 일치하는 도구를 찾았다면 해당 도구를 실행합니다.\n",
    "            result = matching_tool.invoke(tool_args)\n",
    "            # 실행 결과를 출력합니다.\n",
    "            print(f\"[실행도구] {tool_name} [Argument] {tool_args}\\n[실행결과] {result}\")\n",
    "        else:\n",
    "            # 일치하는 도구를 찾지 못했다면 경고 메시지를 출력합니다.\n",
    "            print(f\"경고: {tool_name}에 해당하는 도구를 찾을 수 없습니다.\")\n",
    "\n",
    "\n",
    "# 도구 호출 실행\n",
    "# 이전에 얻은 tool_call_results를 인자로 전달하여 함수를 실행합니다.\n",
    "execute_tool_calls(tool_call_results)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## bind_tools + Parser + Execution\n",
    "\n",
    "이번에는 일련의 과정을 한 번에 실행합니다.\n",
    "\n",
    "- `llm_with_tools` : 도구를 바인딩한 모델\n",
    "- `JsonOutputToolsParser` : 도구 호출 결과를 파싱하는 파서\n",
    "- `execute_tool_calls` : 도구 호출 결과를 실행하는 함수\n",
    "\n",
    "**흐름 정리**\n",
    "1. 모델에 도구를 바인딩\n",
    "2. 도구 호출 결과를 파싱\n",
    "3. 도구 호출 결과를 실행"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.output_parsers.openai_tools import JsonOutputToolsParser\n",
    "\n",
    "# bind_tools + Parser + Execution\n",
    "chain = llm_with_tools | JsonOutputToolsParser(tools=tools) | execute_tool_calls"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 실행 결과\n",
    "chain.invoke(\"What is the length of the word 'teddynote'?\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 실행 결과\n",
    "chain.invoke(\"114.5 + 121.2\")\n",
    "print(114.5 + 121.2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 실행 결과\n",
    "chain.invoke(\n",
    "    \"뉴스 기사 내용을 크롤링해줘: https://n.news.naver.com/mnews/hotissue/article/092/0002347672?type=series&cid=2000065\"\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## bind_tools > Agent & AgentExecutor 로 대체\n",
    "\n",
    "`bind_tools()` 는 모델에 사용할 수 있는 스키마(도구)를 제공합니다. \n",
    "\n",
    "`AgentExecutor` 는 실제로 llm 호출, 올바른 도구로 라우팅, 실행, 모델 재호출 등을 위한 실행 루프를 생성합니다.\n",
    "\n",
    "**참고**\n",
    "- `Agent` 와 `AgentExecutor` 에 대해서는 다음 장에서 자세히 다룹니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "# Agent 프롬프트 생성\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\n",
    "            \"system\",\n",
    "            \"You are very powerful assistant, but don't know current events\",\n",
    "        ),\n",
    "        (\"user\", \"{input}\"),\n",
    "        MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n",
    "    ]\n",
    ")\n",
    "\n",
    "# 모델 생성\n",
    "llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.agents import create_tool_calling_agent\n",
    "from langchain.agents import AgentExecutor\n",
    "\n",
    "# 이전에 정의한 도구 사용\n",
    "tools = [get_word_length, add_function, naver_news_crawl]\n",
    "\n",
    "# Agent 생성\n",
    "agent = create_tool_calling_agent(llm, tools, prompt)\n",
    "\n",
    "# AgentExecutor 생성\n",
    "agent_executor = AgentExecutor(\n",
    "    agent=agent,\n",
    "    tools=tools,\n",
    "    verbose=True,\n",
    "    handle_parsing_errors=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Agent 실행\n",
    "result = agent_executor.invoke({\"input\": \"How many letters in the word `teddynote`?\"})\n",
    "\n",
    "# 결과 확인\n",
    "print(result[\"output\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Agent 실행\n",
    "result = agent_executor.invoke({\"input\": \"114.5 + 121.2 의 계산 결과는?\"})\n",
    "\n",
    "# 결과 확인\n",
    "print(result[\"output\"])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "한 번의 실행으로 끝나는 것이 아닌, 모델이 자신의 결과를 확인하고 다시 자신을 호출하는 과정을 거칩니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Agent 실행\n",
    "result = agent_executor.invoke(\n",
    "    {\"input\": \"114.5 + 121.2 + 34.2 + 110.1 의 계산 결과는?\"}\n",
    ")\n",
    "\n",
    "# 결과 확인\n",
    "print(result[\"output\"])\n",
    "print(\"==========\\n\")\n",
    "print(114.5 + 121.2 + 34.2 + 110.1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "이번에는 뉴스 결과를 크롤링 해서 요약 해달라는 요청을 수행합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result = agent_executor.invoke(\n",
    "    {\n",
    "        \"input\": \"뉴스 기사를 요약해 줘: https://n.news.naver.com/mnews/hotissue/article/092/0002347672?type=series&cid=2000065\"\n",
    "    }\n",
    ")\n",
    "print(result[\"output\"])"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
